/**JavaFile******************************************************************

  FileName    [Program that implements the Santa Claus Problem]

  Synopsis [ Santa Claus Problem template ]

  Author      [Iria Estevez-Ayres <ayres@it.uc3m.es>]

  Copyright   [Copyright (c) 2019 Carlos III University of Madrid
  All rights reserved.

  Permission is hereby granted, without written agreement and without license
  or royalty fees, to use, copy, modify, and distribute this software and its
  documentation for any purpose, provided that the above copyright notice and
  the following two paragraphs appear in all copies of this software.

  IN NO EVENT SHALL THE CARLOS III UNIVERSITY OF MADRID BE LIABLE TO ANY PARTY
  FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
  OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF THE CARLOS III
  UNIVERSITY OF MADRID HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

  THE CARLOS III UNIVERSITY OF MADRID SPECIFICALLY DISCLAIMS ANY WARRANTIES,
  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
  FITNESS FOR A PARTICULAR PURPOSE.  THE SOFTWARE PROVIDED HEREUNDER IS ON AN
  "AS IS" BASIS, AND CARLOS III UNIVERSITY OF MADRID HAS NO OBLIGATION TO
  PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.]

 ******************************************************************************/

import java.lang.*;
import java.util.*;

import java.util.concurrent.Semaphore;

public class NorthPole{

    public static void main(String[] args)
    {

        // You can (and should) create new constructors for each class
        // So, you will also need to modify this main

        Santa claus = new Santa();
        Reindeer rudolf;
        Elf legolas;

        claus.start();

        for (int i=0; i < 9; i++){
            rudolf = new Reindeer();
            rudolf.start();
        }

        for (int i=0; i < 12; i++){
            legolas = new Elf();
            legolas.start();
        }

    }
}

abstract class HandledThread extends Thread {

    public abstract void run();

    protected void handleAcquire(Semaphore s)
    {
        try {
            s.acquire();
        } catch (InterruptedException e) {
            e.printStackTrace();
            System.exit(1);
        }
    }

    protected void handleSleep(long millis)
    {
        try {
            sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
            System.exit(1);
        }
    }
}


class Santa extends HandledThread{
    public static Semaphore wakey = new Semaphore(0);
    public void run(){
        while (true)
        {
            System.out.println("Santa is sleeping peacefully in his bed.");
            handleAcquire(wakey);
            if (Reindeer.nReindeer == Reindeer.N_REINDEER)
            {
                System.out.println("The reindeer are ready. Santa's on his way to give out the presents!");
                Reindeer.nReindeer = 0;
                travel();
            }
            else
            {
                System.out.println("Some elves have a question! Better solve that issue now.");
                consult();
            }
        }
    }

    private void travel()
    {
        for(int i = 0; i < Reindeer.N_REINDEER; ++i)
            Reindeer.bReady.release();
        handleSleep(5000);
        System.out.println("Santa has finished travelling around the world!");
        for(int i = 0; i < Reindeer.N_REINDEER; ++i)
            Reindeer.bDone.release();
    }

    private void consult()
    {
        int elvesConsulted = 0;
        handleSleep(1000);
        while (elvesConsulted < 3)
        {
            Elf.bIn.release();
            handleSleep(5000);
            Elf.nElvesWaiting -= 1;
            Elf.bOut.release();
            elvesConsulted += 1;
        }
    }
}

class Elf extends HandledThread{
    public static final long ELF_AVG_WORK_TIME = 6000, ELF_MIN_WORK_TIME = 2000;
    public static final int ELVES_IN_GROUP = 3;
    public static Semaphore sGroup = new Semaphore(ELVES_IN_GROUP),
           bIn = new Semaphore(0),
           bOut = new Semaphore(0);
    public static int nElvesWaiting = 0;
    public static boolean somebodyAskedSanta = false;

    public void run()
    {
        while (true)
        {
            handleSleep((long) (Math.random() * ELF_AVG_WORK_TIME) + ELF_MIN_WORK_TIME);
            System.out.println("\t\tElf number " + getId() + " has a question");

            formGroup();
            goAskQuestion();
            sGroup.release();
        }
    }

    private void formGroup()
    {
        handleAcquire(sGroup);
        System.out.println("\t\tElf number " + getId() + " is waiting in the group");
        synchronized (this.getClass())
        {
            nElvesWaiting += 1;
        }
    }

    private void goAskQuestion()
    {
        synchronized (this.getClass())
        {
            if (somebodyAskedSanta) somebodyAskedSanta = false;
        }

        if (nElvesWaiting == ELVES_IN_GROUP && !somebodyAskedSanta)
        {
            Santa.wakey.release();
            synchronized (this.getClass())
            {
                somebodyAskedSanta = true;
            }
        }
        handleAcquire(bIn);
        System.out.println("\t\tElf number " + getId() + " is asking Santa");
        handleSleep(1000);
        handleAcquire(bOut);
        System.out.println("\t\tElf number " + getId() + " has finished his enquiry.");
    }
}

class Reindeer extends HandledThread {

    public static final int N_REINDEER = 9;
    public static final long REINDEER_AVG_MUNCH_TIME = 8000,
           REINDEER_MIN_MUNCH_TIME = 2000;

    public static Semaphore mNumReindeer = new Semaphore(1),
           bReady = new Semaphore(0),
           bDone = new Semaphore(0);
    public static int nReindeer = 0;

    public void run()
    {
        while (true)
        {
            handleSleep((long) (Math.random() * REINDEER_AVG_MUNCH_TIME) + REINDEER_MIN_MUNCH_TIME);
            System.out.println("\tReindeer " + getId() + " has finished resting.");
            goBackToStable();
            //Once reached this point, if Santa has been awaken, the reindeer will
            // get into the sleigh all at once.
            travel();
            System.out.println("\tReindeer " + getId() + " is back home.");
        }
    }

    private void goBackToStable()
    {
        handleAcquire(mNumReindeer);
        nReindeer += 1;
        if (nReindeer == N_REINDEER)
        {
            Santa.wakey.release();
        }
        mNumReindeer.release();
    }

    private void travel()
    {
        handleAcquire(bReady);
        handleAcquire(bDone);
    }
}

